package pl.enigmatic.fsm;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

import pl.enigmatic.math.BijectionMap;
import pl.enigmatic.util.Arrays;


/**
 * @param <S> state
 * @param <E> event
 */
public class StateMachine<S extends Enum<S>, E extends Enum<E>, T> {

	// ==================================
	/** states type class */
	protected final Class<S> states;
	/** events type class */
	protected final Class<E> events;
	protected final T target;

	/** a map of allowed state transitions */
	private final Map<S, Set<S>> allowedTransitions = new HashMap<S, Set<S>>();
	/** a map of events allowed to be triggered in states */
	private final Map<S, Set<E>> allowedEvents = new HashMap<S, Set<E>>();
	/** a map of state transitions in response to particular events */
	private final Map<E, Map<S, S>> actionTriggers = new HashMap<E, Map<S, S>>();

	// TODO poprawić to
	/** a linear ordering of states for */
	private final BijectionMap<S, S> linear = new BijectionMap<S, S>();

	/** current state */
	private S current = null;

	// ==================================
	private final List<BeforeEventHandler.General<E, T>> beforeEventGeneralHandlers = new Vector<BeforeEventHandler.General<E, T>>();
	private final Map<E, List<BeforeEventHandler<T>>> beforeEventHandlers = new HashMap<E, List<BeforeEventHandler<T>>>();
	private final List<AfterEventHandler.General<E, T>> afterEventGeneralHandlers = new Vector<AfterEventHandler.General<E, T>>();
	private final Map<E, List<AfterEventHandler<T>>> afterEventHandlers = new HashMap<E, List<AfterEventHandler<T>>>();

	// ==================================
	private final Map<S, List<OnStateExitedListener<S, T>>> onStateExitedListeners = new HashMap<S, List<OnStateExitedListener<S, T>>>();
	private final List<OnStateTransitionedListener.General<S, T>> onStateTransitionedGeneralListeners = new Vector<OnStateTransitionedListener.General<S, T>>();
	private final Map<S, Map<S, List<OnStateTransitionedListener<T>>>> onStateTransitionedListeners = new HashMap<S, Map<S, List<OnStateTransitionedListener<T>>>>();
	private final Map<S, List<OnStateEnteredListener<S, T>>> onStateEnteredListeners = new HashMap<S, List<OnStateEnteredListener<S, T>>>();

	// ==================================
	private final Set<Restrictor<S, E, T>> restrictors = new HashSet<Restrictor<S, E, T>>();

	// ======================================================================================================================================================
	// initialization
	// ======================================================================================================================================================

	public StateMachine(final Class<S> states, final Class<E> events, final T target) {
		this.states = states;
		this.events = events;
		this.target = target;
		for (final E e : events.getEnumConstants()) {
			actionTriggers.put(e, new HashMap<S, S>());

			beforeEventHandlers.put(e, new Vector<BeforeEventHandler<T>>());
			afterEventHandlers.put(e, new Vector<AfterEventHandler<T>>());
		}
		for (final S s : states.getEnumConstants()) {
			allowedTransitions.put(s, new HashSet<S>());
			allowedEvents.put(s, new HashSet<E>());

			onStateExitedListeners.put(s, new Vector<OnStateExitedListener<S, T>>());
			onStateEnteredListeners.put(s, new Vector<OnStateEnteredListener<S, T>>());

			final Map<S, List<OnStateTransitionedListener<T>>> l = new HashMap<S, List<OnStateTransitionedListener<T>>>();
			onStateTransitionedListeners.put(s, l);
			for (final S to : states.getEnumConstants()) {
				l.put(to, new Vector<OnStateTransitionedListener<T>>());
			}
		}
	}

	protected void setInitialState(final S init) {
		current = init;
		for (final OnStateTransitionedListener.General<S, T> l : onStateTransitionedGeneralListeners) {
			l.onTransitioned(current, current, target);
		}
		for (final OnStateEnteredListener<S, T> l : onStateEnteredListeners.get(current)) {
			l.onEntered(current, target);
		}
	}

	public E[] enumEvents() {
		return events.getEnumConstants();
	}

	public S[] enumStates() {
		return states.getEnumConstants();
	}

	// state transitions

	protected void allowStateTransitions(final S from, final S... to) {
		for (final S s : to) {
			allowedTransitions.get(from).add(s);
		}
	}

	@SuppressWarnings("unchecked")
	protected void allowTransitionsPath(final S... path) {
		for (int i = 0; i < path.length - 1; i++) {
			allowStateTransitions(path[i], path[i + 1]);
		}
	}

	@SuppressWarnings("unchecked")
	protected void allowTransitionsCycle(final S... cycle) {
		if (cycle.length > 1) {
			allowTransitionsPath(cycle[cycle.length - 1], cycle[0]);
			allowTransitionsPath(cycle);
		}
	}

	protected void allowTransitionsPathTwoWays(final S... path) {
		allowTransitionsPath(path);
		allowTransitionsPath(Arrays.inverse(path));
	}

	protected void allowTransitionsCycleTwoWays(final S... cycle) {
		allowTransitionsCycle(cycle);
		allowTransitionsCycle(Arrays.inverse(cycle));
	}

	// linear path\cycle
	// FIXME poprawić i przemyśleć
	protected void addLinearPath(final S... path) {
		allowTransitionsPath(path);
		for (int i = 0; i < path.length - 1; i++) {
			linear.putXY(path[i], path[i + 1]);
		}
	}

	@SuppressWarnings("unchecked")
	protected void addLinearCycle(final S... cycle) {
		if (cycle.length > 1) {
			addLinearPath(cycle[cycle.length - 1], cycle[0]);
			addLinearPath(cycle);
		}
	}

	protected void addLinearPathTwoWays(final S... path) {
		addLinearPath(path);
		addLinearPath(Arrays.inverse(path));
	}

	protected void addLinearCycleTwoWays(final S... cycle) {
		addLinearCycle(cycle);
		addLinearCycle(Arrays.inverse(cycle));
	}

	// events

	protected void allowEvents(final S from, final E... to) {
		for (final E e : to) {
			allowedEvents.get(from).add(e);
		}
	}

	@SuppressWarnings("unchecked")
	protected void bindEventToState(final E e, final S from, final S to) {
		actionTriggers.get(e).put(from, to);
		allowEvents(from, e);
		allowStateTransitions(from, to);
	}

	protected void bindEventToState(final E e, final S to) {
		for (final S from : states.getEnumConstants()) {
			bindEventToState(e, from, to);
		}
	}

	// ======================================================================================================================================================
	// listeners bindings
	// ======================================================================================================================================================

	public <H extends BeforeEventHandler.General<E, T>> H addGeneralBeforeEventHandler(final H h) {
		beforeEventGeneralHandlers.add(h);
		return h;
	}

	public <H extends BeforeEventHandler<T>> H addBeforeEventHandler(final E e, final H h) {
		beforeEventHandlers.get(e).add(h);
		return h;
	}

	public <H extends AfterEventHandler.General<E, T>> H addGeneralAfterEventHandler(final H h) {
		afterEventGeneralHandlers.add(h);
		return h;
	}

	public <H extends AfterEventHandler<T>> H addAfterEventHandler(final E e, final H h) {
		afterEventHandlers.get(e).add(h);
		return h;
	}

	public <L extends OnStateEnteredListener<S, T>> L addStateEnteredListener(final S s, final L l) {
		onStateEnteredListeners.get(s).add(l);
		return l;
	}

	public <L extends OnStateTransitionedListener.General<S, T>> L addGeneralStateTransitionedListener(final L l) {
		onStateTransitionedGeneralListeners.add(l);
		return l;
	}

	public <L extends OnStateTransitionedListener<T>> L addStateTransitionedListener(final S from, final S to, final L l) {
		onStateTransitionedListeners.get(from).get(to).add(l);
		return l;
	}

	public <L extends OnStateExitedListener<S, T>> L addStateExitedListener(final S s, final L l) {
		onStateExitedListeners.get(s).add(l);
		return l;
	}

	public <R extends Restrictor<S, E, T>> R addRestrictor(final R r) {
		restrictors.add(r);
		return r;
	}

	// ======================================================================================================================================================
	// state transitions and events propagating
	// ======================================================================================================================================================

	public boolean setCurrent(final S desired) {
		if (!isReachable(desired)) { return false; }
		final S old = current;
		current = desired;

		for (final OnStateExitedListener<S, T> l : onStateExitedListeners.get(old)) {
			l.onExited(current, target);
		}
		for (final OnStateTransitionedListener<T> l : onStateTransitionedListeners.get(old).get(current)) {
			l.onTransitioned(target);
		}
		for (final OnStateTransitionedListener.General<S, T> l : onStateTransitionedGeneralListeners) {
			l.onTransitioned(old, current, target);
		}
		for (final OnStateEnteredListener<S, T> l : onStateEnteredListeners.get(current)) {
			l.onEntered(current, target);
		}

		return true;
	}

	/**
	 * @return the state after this call
	 */
	public final S next() {
		if (linear.containsX(current)) {
			setCurrent(linear.getY(current));
		}
		return current;
	}

	/**
	 * @return the state after this call
	 */
	public final S prev() {
		if (linear.containsY(current)) {
			setCurrent(linear.getX(current));
		}
		return current;
	}

	public boolean fireEvent(final E e) {
		if (!isActionAllowed(e)) { return false; }
		for (final BeforeEventHandler.General<E, T> h : beforeEventGeneralHandlers) {
			h.beforeEvent(e, target);
		}
		for (final BeforeEventHandler<T> h : beforeEventHandlers.get(e)) {
			h.beforeEvent(target);
		}

		setCurrent(actionTriggers.get(e).get(current));

		for (final AfterEventHandler.General<E, T> h : afterEventGeneralHandlers) {
			h.afterEvent(e, target);
		}
		for (final AfterEventHandler<T> h : afterEventHandlers.get(e)) {
			h.afterEvent(target);
		}
		return true;
	}

	// ======================================================================================================================================================
	// internal state properties getters
	// ======================================================================================================================================================

	public boolean isReachable(final S state) {
		boolean res = current == null || state != null && allowedTransitions.get(current).contains(state);
		final Iterator<Restrictor<S, E, T>> i = restrictors.iterator();
		while (res && i.hasNext()) {
			final Restrictor<S, E, T> r = i.next();
			res = r.canEnter(state, target) && r.canExit(current, target) && !r.isFrozen(target);
		}
		return res;
	}

	public boolean isActionAllowed(final E e) {
		boolean res = allowedEvents.get(current).contains(e) && actionTriggers.containsKey(e) && isReachable(actionTriggers.get(e).get(current));
		final Iterator<Restrictor<S, E, T>> i = restrictors.iterator();
		while (res && i.hasNext()) {
			res = i.next().canExecute(e, target);
		}
		return res;
	}

	public Class<S> getStates() {
		return states;
	}

	public Class<E> getEvents() {
		return events;
	}

	public S getCurrent() {
		return current;
	}

}
